package com.heima.wemedia.service.impl;

import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.heima.apis.article.IArticleClient;
import com.heima.common.aliyun.GreenImageScan;
import com.heima.common.aliyun.GreenTextScan;
import com.heima.common.tess4j.Tess4jClient;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.wemedia.pojos.WmChannel;
import com.heima.model.wemedia.pojos.WmNews;
import com.heima.model.wemedia.pojos.WmSensitive;
import com.heima.model.wemedia.pojos.WmUser;
import com.heima.utils.common.SensitiveWordUtil;
import com.heima.wemedia.mapper.WmChannelMapper;
import com.heima.wemedia.mapper.WmNewsMapper;
import com.heima.wemedia.mapper.WmSensitiveMapper;
import com.heima.wemedia.mapper.WmUserMapper;
import com.heima.wemedia.service.WmNewsAutoScanService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.util.*;
import java.util.stream.Collectors;


@Service//标识为业务逻辑层，会被spring加载成被其管理的bean对象
@Slf4j//日志打印
@Transactional//因为涉及到多张表的操作，所以需要事务
public class WmNewsAutoScanServiceImpl implements WmNewsAutoScanService {

	@Autowired
	private WmNewsMapper wmNewsMapper;

	/**
	 * 自媒体文章审核，文章发布后，会异步进行审核文章的内容，包含文字和图片
	 *
	 * @param id 自媒体文章id
	 */
	@Override
	@Async//异步调用，防止主线程阻塞，可以提升系统性能
	//因为文章发布后，并不是立即审核的，这里可以使用异步，提升系统系统
	//根据文章id自动审核自媒体文章
	public void autoScanWmNews(Integer id) {

		//1.查询自媒体文章，根据前端传递的id去数据库中查询数据
		WmNews wmNews = wmNewsMapper.selectById(id);

		//对从数据库中查询的数据进行判断，如果不存在，则抛出异常
		//一般从数据库中查询的数据，都以DB结尾进行标识，我们都要判断其是否为null
		if (wmNews == null) {
			//文章不存在，抛出异常，结束审核流程
			throw new RuntimeException("WmNewsAutoScanServiceImpl-文章不存在");
		}

		//如果文章状态为已提交待审核状态，则进行审核;判断文章状态是否为1
		//判断我们从数据库中查询出来的对象的状态是否为待审核状态，即状态是否为1
		if (wmNews.getStatus().equals(WmNews.Status.SUBMIT.getCode())) {
			//从内容中提取纯文本内容和图片
			Map<String, Object> textAndImages = handleTextAndImages(wmNews);

			//自管理的敏感词过滤，获取刚刚提取的内容并转换成字符串，作为参数调用敏感词管理方法进行判断
			boolean isSensitive = handleSensitiveScan((String) textAndImages.get("content"), wmNews);
			//如果文章内容包含敏感词，则直接结束方法；否则，继续执行下面操作
			if (!isSensitive) return;

			//2.审核文本内容  阿里云接口，将文章内容作为参数去阿里云接口审核
			boolean isTextScan = handleTextScan((String) textAndImages.get("content"), wmNews);
			//如果审核失败，则直接返回，不进行后续审核，提前结束审核流程
			if (!isTextScan) return;

			//3.审核图片  阿里云接口
			boolean isImageScan = handleImageScan((List<String>) textAndImages.get("images"), wmNews);
			//如果审核失败，则直接返回，不进行后续审核，提前结束审核流程
			if (!isImageScan) return;

			//4.审核成功，保存app端的相关的文章数据
			ResponseResult responseResult = saveAppArticle(wmNews);
			//如果返回结果是code不为200，则提示审核失败
			if (!responseResult.getCode().equals(200)) {
				throw new RuntimeException("WmNewsAutoScanServiceImpl-文章审核，保存app端相关文章数据失败");
			}

			//审核成功的话，需要回填article_id
			wmNews.setArticleId((Long) responseResult.getData());
			//更新文章数据
			updateWmNews(wmNews, (short) 9, "审核成功");

		}
	}

	@Autowired
	private WmSensitiveMapper wmSensitiveMapper;

	/**
	 * 自管理的敏感词审核
	 * 通过对文章的内容文本进行审核，查看是否有敏感词
	 *
	 * @param content
	 * @param wmNews
	 * @return
	 */
	private boolean handleSensitiveScan(String content, WmNews wmNews) {
		//定义一个布尔类型的变量flag，并初始化为true（旗帜思想）
		boolean flag = true;

		//这行代码的最终结果是将wmSensitives列表中的每个WmSensitive对象的sensitives字段值提取出来，
		//并存储到一个新的List<String>类型的列表中，这个列表就是最终的敏感词列表
		//selectList()方法用于从数据库中查询多条记录，并返回一个List<WmSensitive>类型的结果
		List<WmSensitive> wmSensitives = wmSensitiveMapper.selectList(Wrappers.<WmSensitive>lambdaQuery().select(WmSensitive::getSensitives));
		//将每个WmSensitive对象的sensitives字段值提取出来，收集到一个集合中
		List<String> sensitiveList = wmSensitives.stream().map(WmSensitive::getSensitives).collect(Collectors.toList());

		//初始化敏感词库，生成关键词字典库
		SensitiveWordUtil.initMap(sensitiveList);

		//查看文章中是否包含敏感词，获取敏感词匹配的关键词和命中次数
		Map<String, Integer> map = SensitiveWordUtil.matchWords(content);
		//如果map集合有值的话，表示有内容中有敏感词存在
		if (map.size() > 0) {
			//调用自定义的更新文章方法，设置新闻对象的状态和原因
			updateWmNews(wmNews, (short) 2, "当前文章中存在违规内容" + map);
			//更新旗帜的属性
			flag = false;
		}

		return flag;
	}

	@Autowired
	private IArticleClient articleClient;

	@Autowired
	private WmChannelMapper wmChannelMapper;

	@Autowired
	private WmUserMapper wmUserMapper;

	/**
	 * 保存app端相关的文章数据
	 *
	 * @param wmNews
	 */
	public ResponseResult saveAppArticle(WmNews wmNews) {
		//创建文章dto对象
		ArticleDto dto = new ArticleDto();
		//属性的拷贝
		BeanUtils.copyProperties(wmNews, dto);
		//文章的布局
		dto.setLayout(wmNews.getType());
		//频道，从数据库中查询频道名称
		WmChannel wmChannel = wmChannelMapper.selectById(wmNews.getChannelId());
		//判断频道是否存在，如果存在，则设置dto的channelName属性
		if (wmChannel != null) {
			dto.setChannelName(wmChannel.getName());
		}

		//作者
		dto.setAuthorId(wmNews.getUserId().longValue());
		//根据作者id查询文章作者名称
		WmUser wmUser = wmUserMapper.selectById(wmNews.getUserId());
		//判断作者是否存在，如果存在，则设置dto的authorName属性
		if (wmUser != null) {
			dto.setAuthorName(wmUser.getName());
		}

		//设置文章id
		if (wmNews.getArticleId() != null) {
			dto.setId(wmNews.getArticleId());
		}
		dto.setCreatedTime(new Date());

		//调用articleClient.saveArticle(dto)方法保存文章数据到app端
		//这是远程调用，需要feign客户端，所以需要在pom.xml中添加依赖
		ResponseResult responseResult = articleClient.saveArticle(dto);
		//返回结果
		return responseResult;

	}


	@Autowired
	private FileStorageService fileStorageService;

	@Autowired
	private GreenImageScan greenImageScan;

	@Autowired
	private Tess4jClient tess4jClient;

	/**
	 * 审核图片
	 *
	 * @param images
	 * @param wmNews
	 * @return
	 */
	private boolean handleImageScan(List<String> images, WmNews wmNews) {

		boolean flag = true;

		if (images == null || images.size() == 0) {
			return flag;
		}

		//下载图片 minIO
		//图片去重
		images = images.stream().distinct().collect(Collectors.toList());

		List<byte[]> imageList = new ArrayList<>();

		//对图片中的文字进行识别和敏感词审核代码
		try {//使用try-catch块来捕获可能出现的异常，以防止程序因异常而崩溃
			for (String image : images) {//通过for循环遍历images列表中的每个图片URL
				//根据每个图片的url地址，从存储服务中下载图片，并将图片内容保存为字节数组（byte[] bytes）
				byte[] bytes = fileStorageService.downLoadFile(image);

				//图片识别文字审核---begin-----

				//从byte[]转换为butteredImage
				//将字节数组转换为ByteArrayInputStream对象，以便可以读取图片数据
				ByteArrayInputStream in = new ByteArrayInputStream(bytes);
				//将字节数组数据读取为BufferedImage对象，这是一种可以在Java中操作和分析图像的类
				BufferedImage imageFile = ImageIO.read(in);
				//调用Tess4J OCR引擎识别BufferedImage对象中的文字，并将识别结果保存为String result
				String result = tess4jClient.doOCR(imageFile);

				//审核是否包含自管理的敏感词，对识别的文字内容进行自管理的敏感词审核
				boolean isSensitive = handleSensitiveScan(result, wmNews);
				//如果发现图片中存在敏感词，则返回false，表示审核失败，结束方法
				if (!isSensitive) {
					return isSensitive;
				}

				//图片识别文字审核---end-----

				//如果识别的文字中不包含敏感词，则将图片的字节数组添加到imageList列表中，以便后续的图片内容审核
				imageList.add(bytes);

			}
		} catch (Exception e) {
			e.printStackTrace();
		}


		//审核图片
		try {
			//调用阿里云的图片审核接口，传入imageList作为参数
			Map map = greenImageScan.imageScan(imageList);
			//如果返回的map不为空，则进一步处理审核结果
			if (map != null) {
				//审核失败,如果map中的suggestion字段等于"block"，则表示图片内容存在违规信息
				if (map.get("suggestion").equals("block")) {
					flag = false;
					//调用updateWmNews(wmNews, (short) 2, "当前文章中存在违规内容")方法更新文章状态为2（审核失败），并设置失败原因
					updateWmNews(wmNews, (short) 2, "当前文章中存在违规内容");
				}

				//不确定信息  需要人工审核
				//果suggestion字段等于"review"，则表示图片内容存在不确定信息，需要人工审核
				if (map.get("suggestion").equals("review")) {
					flag = false;
					//调用updateWmNews(wmNews, (short) 3, "当前文章中存在不确定内容")方法更新文章状态为3（审核不确定），并设置失败原因
					updateWmNews(wmNews, (short) 3, "当前文章中存在不确定内容");
				}
			}

		} catch (Exception e) {
			//如果出现异常，则认为图片内容审核失败，将flag设为false，并打印堆栈跟踪信息
			flag = false;
			e.printStackTrace();
		}
		return flag;
	}

	@Autowired
	private GreenTextScan greenTextScan;

	/**
	 * 审核纯文本内容
	 *
	 * @param content
	 * @param wmNews
	 * @return
	 */
	private boolean handleTextScan(String content, WmNews wmNews) {
		//定义一个boolean类型的变量flag，并初始化为true，表示默认情况下认为文章文本内容是合格的，不需要人工审核
		boolean flag = true;
		//检查自媒体文章的标题和内容的总长度是否为0
		if ((wmNews.getTitle() + "-" + content).length() == 0) {
			//如果标题和内容都为空，则直接返回flag的值true，表示不需要进行审核
			//return结束的是整个方法
			return flag;
		}
		//调用阿里云接口进行文本内容审核
		try {
			//调用greenTextScan对象的greeTextScan方法，将文章标题和内容拼接后作为参数进行文本审核
			Map map = greenTextScan.greeTextScan((wmNews.getTitle() + "-" + content));
			//如果返回的Map对象不为空，则开始处理审核结果
			if (map != null) {
				//审核失败
				//检查审核结果中的suggestion字段是否等于"block"。如果是，则表示文本内容存在违规信息
				if (map.get("suggestion").equals("block")) {
					flag = false;
					//调用updateWmNews方法更新文章状态为2（审核失败），并设置失败原因
					updateWmNews(wmNews, (short) 2, "当前文章中存在违规内容");
				}

				//不确定信息的话，需要人工审核
				if (map.get("suggestion").equals("review")) {
					flag = false;
					//调用updateWmNews方法更新文章状态为3（审核不确定），并设置失败原因
					updateWmNews(wmNews, (short) 3, "当前文章中存在不确定内容");
				}
			}
		} catch (Exception e) {
			//如果出现异常，则认为文章文本内容审核失败，设置flag为false
			flag = false;
			e.printStackTrace();
		}
		//返回flag的值，表示是否需要进行人工审核
		return flag;
	}

	/**
	 * 修改文章内容
	 *
	 * @param wmNews
	 * @param status
	 * @param reason
	 */
	//这是抽取出来的一个方法，因为在该类中多次引用，所以抽取出来了，提高代码的复用性
	//该方法的作用是根据传入的文章状态和失败原因，更新文章的状态和失败原因
	private void updateWmNews(WmNews wmNews, short status, String reason) {
		wmNews.setStatus(status);
		wmNews.setReason(reason);
		//根据文章对象，更新数据库中的文章状态和失败原因
		wmNewsMapper.updateById(wmNews);
	}

	/**
	 * 1.从数据库中自媒体文章的内容中提取文本和图片
	 * 因为涉及到两个不同的数据，所以需要用map进行封装，如果只有一种数据，可以用list集合接收
	 * 2.提取文章的封面图片（封面图片不在内容中，所以需要特别操作一下）
	 *
	 * @param wmNews
	 * @return
	 */
	private Map<String, Object> handleTextAndImages(WmNews wmNews) {

		//定义一个StringBuilder来存储纯文本内容
		StringBuilder stringBuilder = new StringBuilder();

		//images 是一个字符串列表，用于存储所有从文章内容和封面图片中提取的图片URL
		List<String> images = new ArrayList<>();

		//1.从自媒体文章的内容中提取文本和图片
		//首先检查自媒体文章的内容是否为空或仅由空白字符组成，如果内容不为空，则解析该内容
		if (StringUtils.isNotBlank(wmNews.getContent())) {
			//因为数据库中是以json形式存储内容，所以这里需要转换成列表的格式
			//利用JSONArray.parseArray将json格式的文章内容转换为List<Map>，方便操作，json->map
			//parseArray 方法的第一个参数是要解析的 JSON 字符串；
			//第二个参数 Map.class 是指解析后的每个 JSON 对象（在 JSON 数组中）应该被转换成 Map 类型的对象。这样可以方便地根据键名来获取对应的值
			List<Map> maps = JSONArray.parseArray(wmNews.getContent(), Map.class);
			//遍历List<Map>，提取文本内容和图片URL
			for (Map map : maps) {
				//如果 type 的值为 "text"，则将 value 的内容追加到 stringBuilder 中
				if (map.get("type").equals("text")) {
					//通过map.get("value")获取value的值，并将其转换为String类型，再追加到 stringBuilder 中
					stringBuilder.append(map.get("value"));
				}
				//一样的，如果 type 的值为 "image",则将 value 的内容添加到 images 中
				if (map.get("type").equals("image")) {
					//通过map.get("value")获取value的值，并将其转换为String类型，再添加到 images 中
					images.add((String) map.get("value"));
				}
			}
		}

		//2.提取文章的封面图片,判断是否有封面图片，如果有，则添加到 images 中
		if (StringUtils.isNotBlank(wmNews.getImages())) {
			//通过split方法将字符串形式的封面图片URL切割成数组，再添加到images中
			//即提取出每个图片的URL，并且用字符串数组的形式存储
			//如果有多张图片的话，是需要进行这个操作的，所以必须加上
			String[] split = wmNews.getImages().split(",");
			//将数组转换为集合，再添加到 images 中
			//此时文章内容的图片和封面的图片都保存在了 images 中
			images.addAll(Arrays.asList(split));
		}

		//3.封装一个Map对象，包含纯文本内容和图片列表，键是字符串对象，值是Java对象
		Map<String, Object> resultMap = new HashMap<>();
		//将 stringBuilder 中的纯文本内容转换为字符串，并放入 resultMap 中，键为 "content"
		resultMap.put("content", stringBuilder.toString());
		//将 images 列表放入 resultMap 中，键为 "images"
		resultMap.put("images", images);
		//返回结果
		return resultMap;
	}
}
